/*
 * 	<pre>
 *	EasyPeas.
 *  Copyright (C) 2007 S Owen
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 * 
 * Jul 24, 2007
 * AnnotatedMappings.java
 * </pre>
 */
package org.easypeas.mappings.annotated;

import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.easypeas.annotations.Accessor;
import org.easypeas.annotations.Action;
import org.easypeas.annotations.EasyPea;
import org.easypeas.annotations.View;
import org.easypeas.exceptions.ForwardNotFoundException;
import org.easypeas.mappings.Mappings;

/**
 * Mappings based upon annotations. This is currently the default for EasyPeas<br>
 * 
 * Mappings are discovered from EasyPeas defined in the URL provided to the
 * constructor, which points to a simple file containing a list of class names.
 * <br>
 * These must be fully qualified class names of classes that have been annotated
 * with &#064EasyPea.<br>
 * 
 * @see EasyPea
 * @see View
 * @see Action
 * @see Accessor
 *
 * @author S Owen
 */
public final class AnnotatedMappings implements Mappings {

	private static Logger logger = Logger.getLogger("org.easypeas.mappings.annotated.AnnotatedMappings");

	private final Map<String, Object> objectMap = new HashMap<String, Object>();

	private final Map<Class<?>, String> keyMap = new HashMap<Class<?>, String>();

	private final Map<Class<?>, Map<String, Method>> actionMap = new HashMap<Class<?>, Map<String, Method>>();

	private final Map<Class<?>, Map<String, Method>> viewMap = new HashMap<Class<?>, Map<String, Method>>();

	private final Map<Class<?>, Map<String, Method>> accessorMap = new HashMap<Class<?>, Map<String, Method>>();

	private final Map<String, String> forwardMap = new HashMap<String, String>();

	private final Map<Method, String> viewTemplateMap = new HashMap<Method, String>();

	/**
	 * Instantiates a new annotated mappings. The URL provided points to a text
	 * file containing a list of fully qualified classnames for the
	 * {@link EasyPea}s to be used in the mappings.
	 * 
	 * @param classesDefinitions
	 *            the classes definitions
	 * 
	 * @throws IOException
	 *             Signals that an I/O exception has occurred.
	 */
	public AnnotatedMappings(URL classesDefinitions) throws IOException {
		init(classesDefinitions);
	}

	/**
	 * {@inheritDoc}
	 */
	public String getViewTemplate(Method method) {
		return viewTemplateMap.get(method);
	}

	/**
	 * {@inheritDoc}
	 */
	public String getForward(Object obj, String method, String forwardName)
			throws ForwardNotFoundException {
		String forwardKey = getName(obj) + "." + method + "." + forwardName;
		String result = forwardMap.get(forwardKey);
		if (result == null) {
			throw new ForwardNotFoundException("Unable to find forward for "
					+ forwardKey);
		}
		return result;
	}

	/**
	 * {@inheritDoc}
	 */
	public String getName(Object obj) {
		return keyMap.get(obj.getClass());
	}

	/**
	 * {@inheritDoc}
	 */
	public Object getObject(String name) {
		return objectMap.get(name);
	}

	/**
	 * {@inheritDoc}
	 */
	public Method getAccessor(Object obj, String name)
			throws NoSuchMethodException {
		Map<String, Method> methodMap = accessorMap.get(obj.getClass());
		if (methodMap == null) {
			throw new NoSuchMethodException("No accessors defined for obj "
					+ obj.getClass());
		}
		Method result = methodMap.get(name);
		if (result == null) {
			throw new NoSuchMethodException("No accessor called " + name
					+ " defined for " + obj.getClass());
		}
		return result;
	}

	/**
	 * {@inheritDoc}
	 */
	public Method getAction(Object obj, String name)
			throws NoSuchMethodException {
		Map<String, Method> methodMap = actionMap.get(obj.getClass());
		if (methodMap == null) {
			throw new NoSuchMethodException("No actions defined for obj "
					+ obj.getClass());
		}
		Method result = methodMap.get(name);
		if (result == null) {
			throw new NoSuchMethodException("No action called " + name
					+ " defined for " + obj.getClass());
		}
		return result;
	}

	/**
	 * {@inheritDoc}
	 */
	public Method getView(Object obj, String name) throws NoSuchMethodException {
		Map<String, Method> methodMap = viewMap.get(obj.getClass());
		if (methodMap == null) {
			throw new NoSuchMethodException("No views defined for obj "
					+ obj.getClass());
		}
		Method result = methodMap.get(name);
		if (result == null) {
			throw new NoSuchMethodException("No view called " + name
					+ " defined for " + obj.getClass());
		}
		return result;
	}

	private void init(URL classesDefinition) throws IOException {
		Properties props = new Properties();
		props.load(classesDefinition.openStream());
		for (Object o : props.keySet()) {
			String className = (String) o;
			try {
				Object obj = Class.forName(className).newInstance();
				logger.finer("Found pea : " + className);
				EasyPea peaAnnotation = (EasyPea) obj.getClass().getAnnotation(
						EasyPea.class);
				if (peaAnnotation != null) {
					String key = peaAnnotation.name();
					addObject(key, obj);
					logger.finer("Class is annotated with key:" + key);
				} else {
					logger.warning("Class is not annotated as Pea:"+className);
				}
			} catch (InstantiationException e) {
				logger.log(Level.SEVERE,"", e);
			} catch (IllegalAccessException e) {
				logger.log(Level.SEVERE,"", e);
			} catch (ClassNotFoundException e) {
				logger.log(Level.SEVERE,"", e);
			}
		}
	}

	private void addObject(String key, Object obj) {
			logger.finer("Adding object:" + obj.getClass() + " with key: "
					+ key);
		keyMap.put(obj.getClass(), key);
		objectMap.put(key, obj);
		for (Method method : obj.getClass().getMethods()) {
			Action action = method.getAnnotation(Action.class);
			View view = method.getAnnotation(View.class);
			Accessor accessor = method.getAnnotation(Accessor.class);

			if (action != null) {
				addAction(obj, method, action);
			}

			if (view != null) {
				addView(obj, method, view);
			}

			if (accessor != null) {
				addAccessor(obj, method, accessor);
			}
		}
	}

	private void addAction(Object obj, Method method, Action action) {
		logger.finer("Adding action method" + method.getName() + " to "
					+ obj.getClass() + ":" + action.name());
		Map<String, Method> methodMap = actionMap.get(obj.getClass());
		if (methodMap == null) {
			methodMap = new HashMap<String, Method>();
			actionMap.put(obj.getClass(), methodMap);
		}
		if (methodMap.containsKey(action.name())) {
			logger.warning("Action " + action.name() + " already defined for "
					+ obj.getClass());
		} else {
			methodMap.put(action.name(), method);
			addForwards(obj, action);
		}
	}

	private void addForwards(Object obj, Action action) {
		for (String forward : action.forwards()) {
			String key = forward.split("=")[0];
			String value = forward.split("=")[1];
			key = getName(obj) + "." + action.name() + "." + key;
			logger.finer("Adding forward: " + key + " -> " + value);
			forwardMap.put(key, value);
		}
	}

	private void addView(Object obj, Method method, View view) {
		logger.finer("Adding view method " + method.getName() + " to "
					+ obj.getClass() + ":" + view.name());
		
		Map<String, Method> methodMap = viewMap.get(obj.getClass());
		if (methodMap == null) {
			methodMap = new HashMap<String, Method>();
			viewMap.put(obj.getClass(), methodMap);
		}
		if (methodMap.containsKey(view.name())) {
			logger.warning("View " + view.name() + " already defined for "
					+ obj.getClass());
		} else {
			methodMap.put(view.name(), method);
			if (view.template().length() > 0) {
				viewTemplateMap.put(method, view.template());
			}
		}
	}

	private void addAccessor(Object obj, Method method, Accessor accessor) {
		logger.finer("Adding accessor method " + method.getName() + " to "
					+ obj.getClass() + ":" + accessor.name());		
		Map<String, Method> methodMap = accessorMap.get(obj.getClass());
		if (methodMap == null) {
			methodMap = new HashMap<String, Method>();
			accessorMap.put(obj.getClass(), methodMap);
		}
		if (methodMap.containsKey(accessor.name())) {
			logger.warning("Accessor " + accessor.name() + " already defined for "
					+ obj.getClass());
		} else {
			methodMap.put(accessor.name(), method);
		}
	}
}
